﻿/*                              
   Copyright 2011 Nils Kopal, Christopher Krüger, Simone Sauer, Dennis Nolte, Uni Duisburg-Essen

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
*/
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Sharebert.SearchLibrary.Interfaces;
using System.Net.Sockets;
using System.Net;

namespace Sharebert.SearchLibrary
{
    public class SearchClient : ISearchClient
    {

        private const int DEFAULT_TIMEOUT = 30;

        public SearchClient(string serverAddress, int serverPort, string listenAddress, int listenPort)
        {
            this.ServerAddress = serverAddress;
            this.ServerPort = serverPort;
            this.ListenAddress = listenAddress;
            this.ListenPort = listenPort;
            this.Timeout = DEFAULT_TIMEOUT;
        }

        /// <summary>
        /// Sends a list of files to the server, that this client wants to offer for download.
        /// </summary>
        /// <param name="files">A list of files to offer</param>
        /// <exception cref="Sharebert.SearchLibrary.SerializationException">Serialization of the offering files failed</exception>
        /// <exception cref="Sharebert.SearchLibrary.NetworkException">Error while sending the search results</exception>
        /// <exception cref="Sharebert.SearchLibrary.NetworkStreamException">Network stream is not writable</exception>
        public void Offer(FileOffer[] files)
        {
            if (this.connection == null || !this.connection.Connected)
            {
                this.connection = CreateConnection();
            }

            FileOfferList list = new FileOfferList(this.ListenAddress, this.ListenPort, files);
            byte[] data = Serialisation.Serialize(list, typeof(FileOfferList));
            Header header = new Header(Message.COMMAND_FILE_OFFER, data.Length);
            IMessage offer = new Message(header, data);

            connection.SendMessage(offer);
            Console.WriteLine(String.Format("Closing connection to {0}:{1}. Local end point was {2}:{3}",
                                            connection.RemoteAddress, connection.RemotePort, connection.LocalAddress, connection.LocalPort));

            // Close the connection, as the server does not send a reply to an offer
            CloseConnection();
        }

        /// <summary>
        /// Sends a search request to the server, requesting for files that match the given regular expression.
        /// </summary>
        /// <param name="regex">The regular expression, used for search</param>
        /// <exception cref="System.ArgumentException">Regex is null or whitespace</exception>
        /// <exception cref="Sharebert.SearchLibrary.NetworkException">Error while sending the search results</exception>
        /// <exception cref="Sharebert.SearchLibrary.NetworkStreamException">Network stream is not writable</exception>
        /// <exception cref="Sharebert.SearchLibrary.SerializationException">Serialization of the offering files failed</exception>
        public void Search(string regex)
        {
            if (String.IsNullOrWhiteSpace(regex))
            {
                throw new ArgumentException("No valid search request!");
            }
            if (this.connection == null || !this.connection.Connected)
            {
                this.connection = CreateConnection();
            }

            SearchRequest sr = new SearchRequest(regex);
            byte[] data = Serialisation.Serialize(sr, typeof(SearchRequest));
            Header header = new Header(Message.COMMAND_SEARCH_REQUEST, data.Length);
            IMessage request = new Message(header, data);

            this.connection.SendMessage(request);
        }

        private IConnection CreateConnection()
        {
            IConnection conn = new Connection();
            conn.ReadTimeout = this.Timeout;
            conn.ConnectionClosed += new EventHandler<EventArgs>(HandleConnectionClosed);
            conn.ConnectionTimedOut += new EventHandler<EventArgs>(HandleConnectionTimedOut);
            conn.MessageReceived += new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            conn.Open(this.ServerAddress, this.ServerPort);
            return conn;
        }

        private void CloseConnection()
        {
            this.connection.ConnectionClosed -= new EventHandler<EventArgs>(HandleConnectionClosed);
            this.connection.ConnectionTimedOut -= new EventHandler<EventArgs>(HandleConnectionTimedOut);
            this.connection.MessageReceived -= new EventHandler<MessageReceivedEventArgs>(HandleMessageReceived);
            this.connection.Close();
        }

        private void HandleConnectionClosed(object sender, EventArgs e)
        {
            CloseConnection();
        }

        private void HandleConnectionTimedOut(object sender, EventArgs e)
        {
            if (this.ConnectionLost != null)
            {
                this.ConnectionLost.Invoke(this, e);
            }
            CloseConnection();
        }

        private void HandleMessageReceived(object sender, MessageReceivedEventArgs e)
        {
            if (e.Message.Header.Command == Message.COMMAND_SEARCH_RESPONSE)
            {
                HandleSearchResult(e.Message);
            }
        }

        private void HandleSearchResult(IMessage message)
        {
            SearchResultList sr = (SearchResultList)Serialisation.Deserialize(message.Payload, typeof(SearchResultList));
            if (this.SearchResultReceived != null)
            {
                this.SearchResultReceived.Invoke(this, new SearchResultEventArgs(sr.ResultList));
            }
            Console.WriteLine(String.Format("Closing connection to {0}:{1}. Local end point was {2}:{3}",
                                            this.connection.RemoteAddress, this.connection.RemotePort, this.connection.LocalAddress, this.connection.LocalPort));
            CloseConnection();
        }


        #region Properties and private member

        public string ServerAddress { get; set; }

        public int ServerPort { get; set; }

        public string ListenAddress { get; set; }

        public int ListenPort { get; set; }

        /// <summary>
        /// Sending and receiving messages will timeout after the specified time in seconds.
        /// </summary>
        public int Timeout { get; set; }

        private IConnection connection;

        #endregion 


        #region Events

        public event EventHandler<EventArgs> ConnectionLost;

        public event EventHandler<SearchResultEventArgs> SearchResultReceived;

        #endregion

    } 
}
